add_alert <- function(yaml_file = "_alerts.yml", alert_name = NULL) {
if (!requireNamespace("yaml", quietly = TRUE)) {
stop("Package 'yaml' is required but not installed.")
}
if (!requireNamespace("rstudioapi", quietly = TRUE)) {
stop("Package 'rstudioapi' is required but not installed.")
}
# Default template
default_alert <- list(
title = "",
type = "",
content = "",
icon = TRUE,
collapse = FALSE,
date_created = I(as.character(Sys.Date())), # <- Force it to be written as string
resolved = FALSE,
date_resolved = "",
resolution = "",
include_extras = FALSE
)
# Custom handlers to force true/false instead of yes/no
custom_handlers <- list(
logical = function(x) if (x) "true" else "false"
)
if (!file.exists(yaml_file)) {
if (is.null(alert_name)) {
new_alert_name <- "alert001"
} else {
new_alert_name <- alert_name
}
alerts_data <- list(alerts_list = list())
alerts_data$alerts_list[[new_alert_name]] <- default_alert
yaml::write_yaml(alerts_data, yaml_file, handlers = custom_handlers)
} else {
alerts_data <- yaml::read_yaml(yaml_file)
if (is.null(alerts_data$alerts_list)) {
alerts_data$alerts_list <- list()
}
n <- length(alerts_data$alerts_list)
if (is.null(alert_name)) {
new_alert_name <- paste0("alert", sprintf("%03d", n + 1))
} else {
new_alert_name <- alert_name
}
alerts_data$alerts_list[[new_alert_name]] <- default_alert
yaml::write_yaml(alerts_data, yaml_file, handlers = custom_handlers)
}
## Fix quoting or boolean madness
## Having such trouble with true/false and yes/no quoted and not. Bah!
x <- readLines(yaml_file)
x <- gsub("'true'", "true", x, fixed = TRUE)
x <- gsub("'false'", "false", x, fixed = TRUE)
writeLines(x, yaml_file)
if (rstudioapi::isAvailable()) {
rstudioapi::documentOpen(normalizePath(yaml_file))
} else {
message("RStudio API not available; please open ", yaml_file, " manually.")
}
cat(paste0(
"To display this alert in your document, copy and paste the following shortcode into your text editor:\n\n",
"::: {#ale-", new_alert_name, "}\n",
"{{< alert '", new_alert_name, "' >}}\n",
":::"
))
}Alert shortcode: utilizing Quarto extensions to create a dynamic system for callouts in statistical reports
1 Abstract
Statistical reports are living documents. They are usually iterative, with changes to the data, the code and presentation of results occuring over the span of the project. Communicating these changes and updates to collaborators can sometimes get lost in the suffle, e.g. email, meetings, Teams messages, etc. The more centralized the record keeping, especially when it comes to data cleaning and analysis, the better. Quarto callouts provide a good medium to highlight important, or new, information within an html report. However, callouts are more often than not a static medium. If the issue that led to the creation of a callout is resolved, it will likely be deleted in the next iteration of the report. The record of that issue–and its resolution–will fade from the document. Rather than searching through meeting minutes or emails to get a sense the evolution of the analysis, what if these echoes of previous issues could live on alongside (or within) the statistical report? The alert shortcode offers an expansion of Quarto callouts, where the creation, tracking and resolution are contained within a project-specific YAML file. This talk will provide some background on Quarto extensions and callouts, before illustrating the creation and implementation of custom Quarto shortcode.
2 Callouts
2.1 About
Callouts are an excellent way to draw extra attention to certain concepts, or to more clearly indicate that certain content is supplemental or applicable to only some scenarios.
2.2 Typical usage
::: {.callout-note}
Note that there are five types of callouts, including:
`note`, `warning`, `important`, `tip`, and `caution`.
:::
::: {.callout-tip}
## Tip with Title
This is an example of a callout with a title.
:::
::: {.callout-caution collapse="true"}
## Expand To Learn About Collapse
This is an example of a 'folded' caution callout that can be expanded by the user. You can use `collapse="true"` to collapse it by default or `collapse="false"` to make a collapsible callout that is expanded by default.
:::
Note that there are five types of callouts, including: note, warning, important, tip, and caution.
This is an example of a callout with a title.
This is an example of a ‘folded’ caution callout that can be expanded by the user. You can use collapse="true" to collapse it by default or collapse="false" to make a collapsible callout that is expanded by default.
2.3 Appearance
Set the appearance attribute in the callout to get this look: {.callout-note appearance="simple"}
2.4 Cross referencing
You can reference callouts anywhere in your document using a hash-ID prefix: #nte, #tip, etc. and then reference the callout using @ syntax.
::: {#tip-example .callout-tip}
## Cross-Referencing a Tip
Add an ID starting with `#tip-` to reference a tip.
:::
See @tip-example...
Add an ID starting with #tip- to reference a tip.
See Tip 1…
3 Quarto extensions
Extensions are a powerful way to modify and extend the behavior of Quarto.
- Shortcodes: Special markdown directives that generate various types of content
- Journal articles: Custom Quarto templates to mimic specific journal formats
- Revealjs plugins: Extend the capabilities of HTML presentations created with Revealjs
- Custom formats: Custom document templates for organization-level reporting or even formatting CVs
4 Quarto shortcodes
Special markdown directives that generate various types of content.
Some are natively supported through Quarto and can be used without any additional installation. While others can be downloaded from external sources (github).
4.1 Native shortcodes
4.1.1 meta
The meta shortcode allows you to insert content from the YAML header, or from an external YAML file _quarto.yml Can print the title of current document using {{< meta title >}}
How it’s written:
The document title is: {{< meta title >}}
How it prints:
The document title is: Alert shortcode: utilizing Quarto extensions to create a dynamic system for callouts in statistical reports
4.1.2 var
In Quarto project, the var shortcode enables you to insert content from a project-level _variables.yml file. Use it to include references to those variables within any document in your project.
Example _variables.yml:
version: 1.2
email:
info: info@example.com
support: support@example.com
This example uses version ?var:version. (Doesn’t work because I’m not in a Quarto Project.)
4.1.3 Lipsum
Add placeholder text to your document
{{< lipsum 1 >}}
Nullam dapibus cursus dolor sit amet consequat. Nulla facilisi. Curabitur vel nulla non magna lacinia tincidunt. Duis porttitor quam leo, et blandit velit efficitur ut. Etiam auctor tincidunt porttitor. Phasellus sed accumsan mi. Fusce ut erat dui. Suspendisse eu augue eget turpis condimentum finibus eu non lorem. Donec finibus eros eu ante condimentum, sed pharetra sapien sagittis. Phasellus non dolor ac ante mollis auctor nec et sapien. Pellentesque vulputate at nisi eu tincidunt. Vestibulum at dolor aliquam, hendrerit purus eu, eleifend massa. Morbi consectetur eros id tincidunt gravida. Fusce ut enim quis orci hendrerit lacinia sed vitae enim.
4.1.4 Placeholder
The {{< placeholder >}} shortcode generates a placeholder image:
4.2 External shortcodes
4.2.1 Font awesome
This extension provides support including free icons provided by Font Awesome.
Installation
To install, simply paste this command into your terminal from your working directory:
quarto add quarto-ext/fontawesome
Then answer a few more prompts to complete the installation. This will install the extension under the _extensions subdirectory.
Some examples
{{< fa thumbs-up >}}
{{< fa folder >}}
{{< fa chess-pawn >}}
{{< fa brands bluetooth >}}
{{< fa brands github size=5x >}}
{{< fa battery-half size=Huge >}}
{{< fa envelope title="An envelope" >}}
4.2.2 Downloadthis
Add download buttons to html files with attached small image/pdf/txt/csv files
4.2.3 Custom callouts
Configure a Quarto Callout with custom values such as its title, icon, icon symbol, color, appearance, and collapsibility.
5 Creating shortcodes
Full documentation here:
With a simple terminal command, Quarto will create templates for your shortcode extension within your chosen directory. To get started, execute quarto create extension shortcode within the parent directory.
After being prompted for a name, several files will be created:
- README.md
- Simple lua code example
- a YAML file
- .gitignore
- An example.qmd file
Shortcodes are written in Lua and there are some references in the Quarto documentation to get you started. Or, you can prompt Chat-GPT and play around with its output to build something that works
6 Alert shortcode
6.1 Background
Oftentimes coming back into a project after months (or years) away, can leave me scratching my head on what was done and why. This is part of my efforts to address that deficiency within my reports.
- Callouts are valuable tools for communicating updates, issues, tips, etc. to the intended audience of the report
- Goal is to ‘alert’ reader to specific information
- Time-sensitive callouts may get deleted as the document evolves
- Thus losing the record of the alert and its resolution
- Gives rise to the concept of an ‘open’ alert or ‘closed’ alerts
- Want to keep a record of closed alerts without digging through commented-out code or version control
- Shortcode framework points to a method of creating callouts, as well as using YAML metadata to store information
6.2 Goals
Step 1: Create shortcode Step 2: ??? Step 3: Profit $$$
- Create a shortcode to create and track callouts using YAML metadata
- Easy to implement and easy to update
- Track status and view all alerts simultaneously
- Able to be cross-referenced
- Recycling of often used callouts to other projects
My previous callout was supposed to have newlines, but YAML formatting is tricky and my Lua code doesn’t support text formatting yet
6.3 Shortcode arguments
true/false are mostly interchangeable with yes/no in YAML, but my Lua code is not the best and so they may not be for the current version of shortcodes. Definitely something worth fixing!
Date Created: 2025-04-15
Resolved: Yes
Date Resolved: 2025-04-24
Resolution: Updated Lua code
- [
alert_name:] Each Alert must have a unique nametitle:The title of the Callout (optional)type:The standard Callout types:warning,tip,note,important,cautioncontent:The text to include in the callouticon:trueorfalse- Include the Callout icon (defaults totrue)collapse:trueorfalse- Whether of not the Callout is collapsed (defaults tofalse)date_created:Date Alert first created (Optional)resolved:trueorfalse- Whether or not the alert is resolved (defaults tono)date_resolved:Date Alert resolved (Optional)resolution:A description of how the alert was resolved (Optional)include_extras:trueorfalse- Whether or not to inlcude alert metadata (resolution status, dates) in the callout (defaults tofalse)
6.4 Within-document YAML
To access the YAML file with the alerts, you will need to add this to the YAML of the report:
metadata-files:
- _alerts.yml
The file can be called anything, but defaults to _alerts.yml. You can also embed the put the YAML code directly into the YAML of your report, but more than a few alerts would introduce a lot of clutter.
6.5 Examples
6.5.1 Simple callout
External YAML structure:
alerts_list:
basic_example:
title: "Simple callout"
type: tip
content: This is 'tip' callout
Alert syntax:
{{< alert "basic_example" >}}
This is ‘tip’ callout
6.5.2 Aside
Can use the .aside syntax to put callouts to the margins:
::: {.aside}
{{< alert "basic_example" >}}
:::
This is ‘tip’ callout
6.5.3 Open alert
open_alert:
title: Critical alert regarding age
type: warning
content: Very high proportion of patients are listed as 99 years old.
icon: true
collapse: false
date_created: '2023-04-01'
resolved: false
include_extras: true
Very high proportion of patients are listed as 99 years old.
Date Created: 2023-04-01
Resolved: No
6.5.4 Closed alert
closed_alert:
title: Critical alert regarding age (closed)
type: tip
content: Very high proportion of patients are listed as 99 years old.
icon: false
collapse: true
date_created: '2025-04-01'
resolved: true
date_resolved: '2025-04-05'
resolution: Discovered that 99 was used to encode missing age at site X. Those
values were set to missing and recalculated age using DOB to confirm remaining
values.
include_extras: true
6.5.5 Cross-linking
To crosslink an alert, you will need to update the YAML options within your document:
crossref:
custom:
- kind: float
reference-prefix: Alert
key: ale
caption-location: top
Crosslink syntax:
:::{#ale-cross}
{{< alert "cross" >}}
:::
Callout:
The trouble with crosslinking is that the text becomes center-justified.
This is the reference for the alert: Alert 1
6.5.6 Cross-linking and aside
This is very important
This is the reference for the alert: Alert 2
6.5.7 Creating new alerts
This system of creating callouts is fairly straightforward, but can it be simpler?
It can! Using the add_alert function provides the framework to create YAML alerts.
The add_alert function takes two arguments:
yaml_filewhich is the name of the YAML file in which to create the alert. Defaults to_alerts.yml. If no YAML file exists in the working directory, one will be created.alert_name: the name of the alert. If no name is supplied, the function will assign a name based on the number of alerts in the YAML file.
Calling add_alert() within the working directory of your qmd file will create a YAML file, open it within your RStudio browser and initialize the first alert like so:
alerts_list:
alert001:
title: ''
type: ''
content: ''
icon: yes
collapse: no
date_created: '2025-04-14'
resolved: no
date_resolved: ''
resolution: ''
The function will also print usage guidance to your console (adding in the crosslink or the aside call are both optional):
To display this alert in your document, copy and paste the following shortcode into your text editor:
::: {#ale-alert001}
Error: Alert ''alert001'' not found.
:::
The function:
6.6 Viewing alerts
Using the parsermd package, and a few of the functions that are found in JDmisc we can ‘find’ all of the alerts in the document and combine their location information with the YAML-encoded alert information.
The read_alerts() function will parse and format all of the alerts within the YAML file as a data frame. Use this information to create an all-in-one dashboard or alerts.
With some extra effort, we can add cross-linking into the display.
Helper functions
read_alerts_df <- function(yaml_file = "_alerts.yml") {
# Load required packages (install if necessary)
if (!requireNamespace("yaml", quietly = TRUE)) {
stop("Package 'yaml' is required but not installed.")
}
if (!requireNamespace("purrr", quietly = TRUE)) {
stop("Package 'purrr' is required but not installed.")
}
if (!requireNamespace("tibble", quietly = TRUE)) {
stop("Package 'tibble' is required but not installed.")
}
# Read the YAML file
alerts_data <- yaml::read_yaml(yaml_file)
# Extract the alerts_list
alerts_list <- alerts_data$alerts_list
# Convert the alerts_list to a dataframe
# Each alert becomes a row; missing fields will become NA
df <- purrr::map_df(names(alerts_list), function(alert_id) {
alert <- alerts_list[[alert_id]]
alert$id <- alert_id # add an id column from the key
tibble::as_tibble(alert)
})
# Optional: reorder columns to have 'id' first
df <- df[, c("id", setdiff(names(df), "id"))]
return(df)
}
grep_alerts <- function(input_string){
pattern <- "\\{\\{<\\s*alert\\s+([^ >]+)\\s*>\\}\\}"
matches <- str_match_all(input_string, pattern)[[1]]
alert_names <- matches[,2]
alert_names
}# Example usage:
alerts_df <- read_alerts_df("_alerts.yml")
parsermd::parse_rmd("shortcode_presentation.qmd") %>%as.data.frame() %>%
mutate(order = cumsum(type == "rmd_heading")) %>% rowwise() %>%
mutate(x = ifelse(type == "rmd_markdown", paste0(parsermd::as_document(ast), collapse = ""), "")) %>%
mutate(has_alert = grepl("\\{\\{< alert", x)) %>%
mutate(y = "") %>%
filter(order >0, has_alert) %>%
left_join(., get_toc("shortcode_presentation.qmd"), by= "order") %>%
mutate(x = gsub('\\"', "", x)) %>%
mutate(id = toString(grep_alerts(x))) %>%
separate_rows(., "id", sep = ",") %>% mutate(id= trimws(id)) %>% ungroup() %>% select(-title, -type) %>%
distinct() %>%
left_join(., alerts_df, by = "id") %>%
mutate(section = case_when(is.na(section) ~ "Not included",
!grepl("#ale", x) ~ glue::glue('§{section}'),
TRUE ~ glue::glue('<a href="#ale-{id}" class="quarto-xref" aria-expanded="false">§{section}</a>') )) %>%
select(section, title, resolved, content, resolution , any_of(c("date_created", "date_resolved"))) %>%
j.reactable(., columns = list(section = colDef(html = TRUE, width = 66),
title = colDef(width = 80),
content = colDef(width = 300),
resolution = colDef(width = 300)),
pagination = FALSE, height = 500)# add_alert(alert_name= "test_fun")7 Installing
Make sure github is up to date before presenting–this includes the example qmd file!
Date Created: 2025-04-15
Resolved: Yes
Date Resolved: 2025-04-24
Resolution: Fixed some lua code, redid the simple example.qmd file
This shortcode is hosted on GitHub
Within terminal, navigate to the working directory of the current project then run the command provided on the GitHub page.
Alternately, run this line and paste into terminal to add alert shortcode extension:
cat("cd ",gsub(" ", "\\\\ ", getwd()), "\nquarto add jjdeclercq/alert_shortcode")
Output from previous command:
cd /Users/joshdeclercq/Documents/GitHub/alert_shortcode
quarto add jjdeclercq/alert_shortcode
This will install the extension under the _extensions subdirectory. If you’re using version control, you will want to check in this directory.